"""
缓存cookies:
    1. 进入首页 , 扫码登录 . ( 不要再次扫码登录 , 因为会造成之前缓存的 cookies 失效 )
    2. get_cookies() 获取到 cookies . (注意一定要在扫码登录成功之后执行该行代码 , time.sleep(30) )
    3. 缓存 cookies 之后 , 要进入登录页面  , 刷新验证是否自动登录成功 .

    获取cookies:
        driver.get_cookies()
    添加cookies:
        for cookie in cookies:
            driver.add_cookie(cookie)


selenium 注意:
    想要获取点击的元素的属性 ,
    一定要先获取属性 , 再进行点击 ,
    因为点击之后如果有页面跳转 , 则可能会出现 StaleElementReferenceException.

StaleElementReferenceException 的解决思路:
    1. 遵循先获取属性 , 再点击元素
    2. 捕获该异常 , 重新获取 ( 可以写一个方法递归来做 )

ElementClickInterceptedException 的解决思路:
    1. 添加等待 ( 解决网络原因造成的该异常 )
    2. js 操作 ( driver.execute_script("arguments[0].click();", WebElement对象) )
    3. ActionChains点击 ( ActionChains(driver).click(WebElement对象) / 甚至是移动到元素之后,在调用click() )
    4. 滚动条 ( 解决因未滚动到元素 , 导致元素被覆盖的情况 )
    5. Enter代替 ( ActionChains(self.driver).move_to_element(mark).key_down(Keys.ENTER).key_up(Keys.ENTER).perform() )
"""
import logging
import time
from typing import List

import yaml

from selenium import webdriver
from selenium.webdriver import ActionChains, Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as e_c
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common import NoSuchElementException, TimeoutException, ElementClickInterceptedException, \
    StaleElementReferenceException
from we_com.common.constants import *


class BasePage:
    _url = None
    __qr = (By.CSS_SELECTOR, '.ww_wechatQrCode')  # 登录二维码

    def __init__(self, driver: webdriver.Chrome = None):
        """
        page 初始化时 , 会根据 cookies 自动登录 ,
        cookies 只在一段时间内有效
        """
        if not driver:
            # options = webdriver.chrome.options.Options()
            # options.debugger_address = 'localhost:9222'
            # self.driver = webdriver.Chrome(options=options)
            self.driver = webdriver.Chrome()
            self.driver.maximize_window()
            self.driver.implicitly_wait(IMPLICITLY_WAIT)

            if self._url:
                self.driver.get(self._url)
                cookies = yaml.safe_load(open(COOKIES_YAML_PATH))

                if cookies:
                    for cookie in cookies:
                        self.driver.add_cookie(cookie)
                    self.driver.get(self._url)

                    if not self.__is_login():
                        self.alert_auto_close('请扫码登录, 15s 后扫码超时')
                        self.__login()
                        self.driver.get(self._url)
                else:
                    self.alert_auto_close('请扫码登录, 15s 后扫码超时')
                    logging.info('首次登录 , 需扫码登录 , 请进行扫码登录')
                    self.__login()
            else:
                logging.error('初始化 Page 缺少 url ...')
        else:
            self.driver = driver

    def __is_login(self) -> bool:
        """
        根据登录页的二维码来判断是否登录
        如果 3s 后 , 首页二维码还在 , 表示没有登录 , 则需要扫码登录
        如果 3s 后 , 首页二维码消失 , 表示已经登录 , return True . 配合 skipif 来跳过执行 .
        .ww_wechatQrCode

        根据首页的 <<退出>> 按钮 , 来判断是否登录成功 . visibility_of_element_located(locator)
        #logout

        已登录 , 返回: True
        未登录 , 返回: False
        """
        try:
            WebDriverWait(self.driver, WAIT_TIMEOUT).until(e_c.invisibility_of_element_located(self.__qr))
            logging.info('已经登录成功 , 不再登录 .')
            return True
        except TimeoutException:
            logging.info('不是登录状态 , 需要扫码登录 .')
            return False

    def __login(self):
        """
        扫码登录企业微信 , 登录完成之后写入 cookies .
        """
        logging.info('扫码登录开始 , 需 15s 内完成')
        try:
            WebDriverWait(self.driver, QR_WAIT).until(e_c.invisibility_of_element_located(self.__qr))
        except TimeoutException:
            logging.error('扫码登录超时')
        else:
            cookies = self.driver.get_cookies()  # 这里认为扫码登录完成 , 写入 cookies .
            with open(COOKIES_YAML_PATH, 'w') as cookie_file:
                yaml.safe_dump(cookies, cookie_file)
                logging.info('扫码登录完成 , 保存 cookies 完成')

    # 关闭浏览器 , 默认 1s 后关闭
    def close(self, timeout=CLOSE_TIME):
        time.sleep(timeout)
        self.driver.close()
        self.driver.quit()
        logging.info('浏览器关闭')

    # 关闭浏览器当前页面 , 不校验是否为多个页面 .
    def close_current(self):
        if len(self.driver.window_handles) > 1:
            logging.info('当前页面关闭')
        else:
            logging.info('浏览器关闭')
        time.sleep(CLOSE_TIME)
        self.driver.close()

    # 强制等待(默认1s)
    @staticmethod
    def sleep(seconds=CLICK_LOAD_SLEEP_TIME):
        """
        强制等待 , 这样子类在进行强制等待的时候 , 就不需要导入 time 模块了 .
        子类对象.sleep(seconds)
        :param
            seconds: 强制等待时间 , 默认 1s
        """
        time.sleep(seconds)
        logging.info(f'强制等待{seconds}秒')

    # 显示等待(默认5s)
    def base_wait(self, timeout=WAIT_TIMEOUT) -> WebDriverWait:
        """
        创建并返回显示等待对象.
        :param timeout: 显示等待超时时间 , 默认 5s
        """
        webdriver_wait = WebDriverWait(self.driver, timeout)
        return webdriver_wait

    # 元素是否可见(invisibility), 不进行元素等待
    @staticmethod
    def is_exist(locator) -> bool:
        """
        返回元素是否存在 , 不进行等待 , 直接返回 .
        :param locator: 定位元素的 locator
        """
        return not e_c.invisibility_of_element_located(locator)

    # 元素是否可见
    def is_exist_visibility(self, locator, timeout=WAIT_TIMEOUT) -> bool:
        """
        元素是否存在 , 只要检查到元素存在就返回 .
        :param
            locator: 定位元素的 locator
            timeout: 超时时间, 默认为5s
        :return: True/False
        """
        try:
            self.base_wait(timeout).until(e_c.visibility_of_element_located(locator))
        except TimeoutException:
            logging.info(f'通过 locator:{locator}, 检查元素: 不存在')
            return False
        else:
            logging.info(f'通过 locator:{locator}, 检查元素: 存在')
            return True

    # 元素是否出现
    def is_exist_presence(self, locator, timeout=WAIT_TIMEOUT) -> bool:
        """
        元素是否存在 , 只要检查到元素存在就返回 .
        :param
            locator: 定位元素的 locator
            timeout: 超时时间, 默认为5s
        :return: True/False
        """
        try:
            self.base_wait(timeout).until(e_c.presence_of_element_located(locator))
        except TimeoutException:
            logging.info(f'通过 locator:{locator}, 检查元素: 不存在')
            return False
        else:
            logging.info(f'通过 locator:{locator}, 检查元素: 存在')
            return True

    # 是否不可见 :
    def is_invisibility(self, locator, timeout=WAIT_TIMEOUT) -> bool:
        """
        元素是否 已经不可见
        :param
            locator : 元素的 locator
            timeout : 默认超时时间为 3s .
        :return :
            True    元素消失
            False   元素存在
        """
        try:
            self.base_wait(timeout).until(e_c.invisibility_of_element_located(locator))
            logging.info(f'元素 locator :{locator}已不可见')
            return True
        except TimeoutException:
            logging.info(f'元素 locator :{locator}仍然可见')
            return False

    # 查找元素 :
    def find(self, locator) -> WebElement:
        """
        根据 locator 查找元素 ,
        记录 locator 和 标签.
        不进行显示等待 .
        """
        try:
            ele = self.driver.find_element(*locator)
        except NoSuchElementException as e:
            logging.info(f'通过 locator :{locator} 定位元素:<--->失败')
            raise e
        else:
            logging.info(f'通过 locator :{locator} 定位元素:<{ele.tag_name}>成功')
            # logging.info(f'通过 locator :{locator} 定位元素:<{ele.get_attribute("outerHTML")}>成功')
            # self.source_html(ele)
            return ele

    # 查找多个元素:
    def find_s(self, locator) -> List[WebElement]:
        """
        查找多个元素 , 以列表形式返回 , 日志记录多个元素的 tag_name
        :param
            locator: 定位多个元素的 locator
        :return:  List[WebElement]
        """
        try:
            ele_s = self.driver.find_elements(*locator)
        except NoSuchElementException as e:
            logging.info(f'通过 locator :{locator}定位多个元素失败, 元素未找到')
            raise e
        else:
            tag_s = str(['<' + ele.tag_name + '>' for ele in ele_s if ele.tag_name])
            logging.info(f'通过 locator :{locator}定位多个元素{tag_s}成功')
            return ele_s

    # 等待元素出现 , 查找并返回元素 :
    def presence(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
            等待元素出现 , 定位并返回元素
        :param
            locator:  元素的 locator
            timeout:  超时时间 , 默认为 3s
        :return:
            元素对象
        """
        try:
            self.base_wait(timeout).until(e_c.presence_of_element_located(locator))
            # ele = self.find(locator)
            ele = self.driver.find_element(*locator)
        except TimeoutException as e:
            logging.info(f'通过 locator :{locator}定位元素:<--->失败')
            raise e
        else:
            logging.info(f'通过 locator :{locator}定位元素:<{ele.tag_name}>成功')
            # logging.info(f'通过 locator :{locator}定位元素:<{ele.get_attribute("outerHTML")}>成功')
            return ele

    # 等待元素可见 , 查找并返回元素 :
    def visibility(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        等待元素可见 , 定位并返回元素 .
        :param
            locator: 元素的 locator
            timeout: 超时时间 , 默认为 3s
        :return:
            元素对象
        :raises
            TimeoutException 查找超时
        """
        try:
            self.base_wait(timeout).until(e_c.visibility_of_element_located(locator))
            # ele = self.find(locator)
            ele = self.driver.find_element(*locator)
        except TimeoutException as e:
            logging.info(f'通过 locator :{locator}定位元素:<--->失败')
            raise e
        else:
            logging.info(f'通过 locator :{locator}定位元素:<{ele.tag_name}>成功')
            # logging.info(f'通过 locator :{locator}定位元素:<{ele.get_attribute("outerHTML")}>成功')
            return ele

    # 点击元素 , 不进行等待 .
    def click(self, mark) -> WebElement:
        """
        点击元素 , 可以直接点击元素 , 也可以根据 locator 点击元素 .
        ( 参考 expected_conditioins.element_to_be_clickable(mark) 中的 mark )
        :param
            mark: mark 可以是 locator 也可以是 WebElement 对象 .
        :return
            WebElement
        """
        tag_name = ''
        try:
            if not isinstance(mark, WebElement):  # locator
                # mark = self.find(mark)
                mark = self.driver.find_element(*mark)
                tag_name = mark.tag_name
            mark.click()
            logging.info(f'点击元素:<{tag_name}>成功')
        except NoSuchElementException as e1:
            logging.info(f'点击元素失败, 元素未找到')
            raise e1
        except ElementClickInterceptedException:
            # ActionChains(self.driver).click(mark).perform()
            # self.move_to_and_enter(mark)
            self.move_to_and_click(mark)
            logging.info(f'点击元素:{mark.tag_name}失败, 元素不可点击, 已使用 Enter 代替点击, 如无效, 请使用 js 或者滑动页面重试')
        except StaleElementReferenceException:
            logging.info(f'点击元素:<-->成功,并且页面发生了跳转.')
        return mark

    # 等待元素可以点击并点击元素:
    def click_clickalbe(self, mark, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        等待元素可以点击 , 并点击元素 .
        :param
            mark:     可以是 locator 也可以是 WebElement 对象 .
            timeout:  超时时间 , 默认是 3s .
        :return
            WebElement
        """
        try:
            self.base_wait(timeout).until(e_c.element_to_be_clickable(mark))
        except TimeoutException as e:
            logging.info(f'点击元素:失败, 元素长时间为不可点击状态')
            raise e
        else:
            return self.click(mark)

    # 出现并点击 :
    def click_presence(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        等待元素出现并点击 .
        :param
            locator: 元素的 locator
            timeout: 超时时间 , 默认 3s .
        :return:
            WebElement
        """
        try:
            self.base_wait(timeout).until(e_c.presence_of_element_located(locator))
        except TimeoutException as e:
            logging.info(f'通过 locator:<{locator}>点击元素: 失败, 元素超时未出现')
            raise e
        else:
            return self.click(locator)

    # 可见并点击 :
    def click_visibility(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        等待元素可见并点击 .
        :param
            locator:  元素的 locator
            timeout:  超时时间 , 默认为 3s .
        :return:
            WebElement
        """
        try:
            self.base_wait(timeout).until(e_c.visibility_of_element_located(locator))
        except TimeoutException as e:
            logging.info(f'通过locator:{locator}点击元素:<<-->>失败, 元素超时不可见')
            raise e
        else:
            return self.click(locator)

    # 点击元素 , 找不到元素则等待元素出现并点击:
    def click_then_presence(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        先点击元素 ,
        定位异常则等待元素出现并点击 .
        :param
            locator:  元素的 locator
            timeout:  超时时间 , 默认为 3s .
        :return:
            WebElement
        """
        try:
            return self.click(locator)
        except NoSuchElementException:
            return self.click_presence(locator, timeout)

    # 点击元素 , 找不到元素则等待元素可见并点击:
    def click_then_visibility(self, locator, timeout=WAIT_TIMEOUT) -> WebElement:
        """
        先点击元素 ,
        定位异常则等待元素可见并点击 .
        :param
            locator:  元素的 locator
            timeout:  超时时间 , 默认为 3s .
        :return:
            WebElement
        """
        try:
            return self.click(locator)
        except NoSuchElementException:
            return self.click_visibility(locator, timeout)

    # 等待并点击 , 强制等待 , 并点击元素:
    def sleep_then_click(self, mark, seconds=CLICK_LOAD_SLEEP_TIME):
        """
        等待 1s 再点击元素
        :param
            mark: 可以是 locator 也可以是 WebElement
            seconds: 强制等待时间 , 默认为 1s
        """
        self.sleep(seconds=seconds)
        return self.click(mark)

    # 移动到元素上, 点击
    def move_to_and_click(self, mark) -> WebElement:
        """
        移动到元素上, 按下回车, 有时点击不可用, 可尝试使用 Enter 替换 .
        :param
            mark: locator 和 WebElement 都可以 .
        """
        if not isinstance(mark, WebElement):
            # mark = self.find(locator=mark)
            mark = self.driver.find_element(*mark)
        a_c = ActionChains(self.driver)
        a_c.move_to_element(mark).pause(0.2).click().perform()
        try:
            logging.info(f'在元素{mark.tag_name}上,点击成功')
        except StaleElementReferenceException:
            logging.info(f'在元素上点击成功')
        return mark

    # 移动到元素上 , 按下回车键 .
    def move_to_and_enter(self, mark) -> WebElement:
        """
        移动到元素上, 按下回车, 有时点击不可用, 可尝试使用 Enter 替换 .
        :param
            mark: locator 和 WebElement 都可以 .
        """
        if not isinstance(mark, WebElement):
            # mark = self.find(locator=mark)
            mark = self.driver.find_element(*mark)
        a_c = ActionChains(self.driver)
        a_c.move_to_element(mark)
        a_c.key_down(Keys.ENTER).pause(0.2).key_up(Keys.ENTER).perform()
        try:
            logging.info(f'在元素{mark.tag_name}上,按下回车成功')
        except StaleElementReferenceException:
            logging.info(f'在元素上,按下回车成功')
        return mark

    # 点击元素 , 返回元素是否消失 .
    def click_and_is_miss(self, mark, timeout=WAIT_TIMEOUT) -> bool:
        """
        点击元素 , 返回元素是否不可见 .
        :param
            mark: 要点击的元素的 locator 或者 要点击的元素对象 .
        :return:
            True:    ENTER 后元素消失 .
            False:   ENTER 后元素仍在 .
        """
        try:
            self.click(mark)
            self.base_wait(timeout).until(e_c.invisibility_of_element_located(mark))
            logging.info('确认元素消失')
            return True
        except TimeoutException:
            logging.info('确认元素仍在')
            return False

    # 定位元素 , 返回元素的文本信息 .
    def get_text(self, mark):
        """
        获取元素的文本信息 , 参数是 locator 时 , 定位元素并获取元素文本信息 .
        当 Element.text 获取不到时 , 会返回 Element.get_attribute('textContent') .
        :param
            mark: 可以是 locator 也可以是 WebElement 对象 .
        :return:
            text: 文本信息 .
        """
        if not isinstance(mark, WebElement):
            # mark = self.find(mark)
            mark = self.driver.find_element(*mark)
        text = mark.text
        if not text:
            text = mark.get_attribute('textContent')  # TODO : 优化兼容 . 这里 IE 不支持 !
        logging.info(f'获取到<{mark.tag_name}>元素的文本:->{text}<-')
        return text

    # 获取元素的 html 源码
    def source_html(self, mark):
        """
        根据 locator 定位元素 , 并返回 html 源码 . ( 常用于定位代码问题 )
        :param
            locator: 获取 html 的 locator
        :return:
            html 源码
        """
        if not isinstance(mark, WebElement):
            mark = self.driver.find_element(*mark)
        source_html = mark.get_attribute('outerHTML')
        logging.info(f'获取到<{mark.tag_name}>元素的 html 源码:--{source_html}--')
        return source_html

    # 截图并保存
    def screenshot(self, filename=''):
        try:
            now = time.strftime("%Y-%m-%d %H_%M_%S")
            screenshot_path = f'{BUG_IMG_PATH}/{filename}_{now}.png'
            self.driver.save_screenshot(screenshot_path)
            logging.info(f'截图保存成功:{screenshot_path}')
        except NameError as e:
            logging.info(f'截图保存失败 .')
            logging.exception(e)

    # 输入内容:
    def input_visibility(self, mark, value: str, timeout=WAIT_TIMEOUT):
        try:
            if not isinstance(mark, WebElement):
                mark = self.visibility(mark, timeout)
            mark.send_keys(value)
            logging.info(f'元素<{mark.tag_name}>输入内容->{value}<-成功')
        except TimeoutException as e:
            logging.info(f'元素<{mark.tag_name}>输入内容->{value}<-失败, 查找元素超时未找到')
            logging.exception(e)
        # except

    # 弹框展示信息之后 , 自动关闭
    def alert_auto_close(self, msg, auto_time=ALERT_TIME):
        """
        进行弹窗警告 , 超时自动关闭
        :param
            msg:       警告信息
            auto_time: 超时时间 , 默认为 2s
        """
        self.driver.execute_script(f'window.alert("{msg}")')
        self.sleep(auto_time)
        self.driver.switch_to.alert.accept()

    # 接受 alert 弹框
    def accept_alert(self):
        self.driver.switch_to.alert.accept()
        """
        # 以下 alert 方法均可选择封装 : 
        self.driver.switch_to.alert.text            # 获取弹框的文本信息
        self.driver.switch_to.alert.accept()        # 接受警告
        self.driver.switch_to.alert.dismiss()       # 忽略警告
        self.driver.switch_to.alert.send_keys(msg)  # 发送文本: msg 给弹框
        """

    # 滑动到顶部
    def scroll_to_top(self):
        js_scroll_to_top = f'window.scrollTo(0,document.body.scrollTop)'
        self.driver.execute_script(js_scroll_to_top)

    # 滑动到底部
    def scroll_to_bottom(self):
        js_gscroll_to_bottom = f'window.scrollTo(0,document.body.scrollHeight)'
        self.driver.execute_script(js_gscroll_to_bottom)

    # 根据 url 切换窗口
    def switch_window_by_url(self, url):
        for handle in self.driver.window_handles:
            self.driver.switch_to.window(handle)
            if self.driver.current_url.__contains__(url):
                break
        self.sleep(1)

    # 根据 title 切换窗口
    def switch_window_by_title(self, title):
        for handle in self.driver.window_handles:
            self.driver.switch_to.window(handle)
            if self.driver.title.__contains__(title):
                break
        self.sleep(1)

    # 输入内容并回车
    # 元素的提交
    # 清除文本框
    # 获取某属性内容
    # 刷新浏览器
    # 三级菜单点击
    # 鼠标操作(右击 , 长按 , 拖拽等 )
    # 下拉选操作(根据值点击 , 根据顺序点击)
    # 窗口切换
    # frame切换
    # js操作(去掉readonly属性,执行js代码等)
    # 判断 url 是否加载
    # 修改元素内容
    # 前往下一个网页
    # 返回之前的网页
